---
-- The beds mod gives players carftable beds that they can lay down in. If a
-- majority of players lay down in bed and 'sleep' at night, then the night
-- will be skipped and the sun will come up. This is a good method to avoid
-- monsters at night.
--
-- This version of the builtin version of the beds mod used mtm.storage to store
-- the list of player's bed spawn positions. This allows only records being
-- modified to be written to disk. Normally bed spawn information is contained
-- in a single table which would then be read in and deserialized even if the
-- objective was to look up a single person spawn position on the map. This
-- table contained every single player who ever right clicked on a bed. When
-- updating a single players spawn location after right clicking on a bed the
-- entire table had to be serialized and written at once. If this process
-- failed the file would be lost, so a backup file was created just in case.
--
-- With mtm.storage, which is just a simple way to automatically create a
-- dbtable storage should be more stable and simple. Data would only be updated
-- for the single record being create, deleted, or updated.
--
-- One popular server had a mod_beds_spawns file of 28k, and a single record
-- was only 65 bytes. This savings will only increase if the mintest serializer
-- becomes more efficent.
--
-- 110 spawn records using the minetest_games beds mod used 5374 bytes and
-- using mtm.storage it took 4931 bytes. The i/o saving would increase wiht the
-- number of records as would the overall size of the stored data.
--
-- @author Originally by BlockMen (MIT)
-- @author Various Minetest developers and contributors (MIT)
-- @author James Dornan james _at_ catch22.com re-write & mtm-storage
-- @copyright Originally by BlockMen (MIT)
-- @copyright Various Minetest developers and contributors (MIT)
-- @copyright James Dornan (GPLv3)
-- @license GPLv3
-- @module beds
--

-- ldoc -f discount -p 'Minetest Beds Mod' -d <output directoru> -a init.lua

-- @section api

---@usage
local usage = [[
beds.register_bed('beds:bed', {
	description = 'Simple Bed',
	inventory_image = 'beds_bed.png',
	wield_image = 'beds_bed.png',
	tiles = {
		bottom = {
			'beds_bed_top_bottom.png^[transformR90',
			'default_wood.png',
			'beds_bed_side_bottom_r.png',
			'beds_bed_side_bottom_r.png^[transformfx',
			'beds_transparent.png',
			'beds_bed_side_bottom.png'
		},
		top = {
			'beds_bed_top_top.png^[transformR90',
			'default_wood.png',
			'beds_bed_side_top_r.png',
			'beds_bed_side_top_r.png^[transformfx',
			'beds_bed_side_top.png',
			'beds_transparent.png',
		}
	},
	nodebox = {
		bottom = {-0.5, -0.5, -0.5, 0.5, 0.06, 0.5},
		top = {-0.5, -0.5, -0.5, 0.5, 0.06, 0.5},
	},
	selectionbox = {-0.5, -0.5, -0.5, 0.5, 0.06, 1.5},
	recipe = {
		{'wool:red', 'wool:red', 'wool:white'},
		{'group:wood', 'group:wood', 'group:wood'}
	},
})
]]

--- Main beds class table
-- @field files table list of files for dofile
-- @field player array list of players who have beds
-- @field num_sleepers int number of players currently sleeping
-- @field pos list of bed postitions for current logged in users
-- @field spawns table object to access database table of player's bed spawn pos
-- @field formspec string beds form
-- @field single_player boolean
-- @field respawn boolean
-- @field night_skip boolean
-- @field path string
local beds = {
	player        = {},
	reverse       = true,
	num_sleepers  = 0,
	pos           = {},
	spawns        = mtm.storage(),
	formspec      = [[
		size[8, 15; true]
		bgcolor[#080808BB; true]
		button_exit[2, 12; 4, 0.75; leave; Leave Bed]
	]],
	single_player = minetest.is_singleplayer(),
	respawn       = minetest.settings:get_bool('enable_bed_respawn') ~= false,
	night_skip    = minetest.settings:get_bool('enable_bed_night_skip') ~= false,
	path          = minetest.get_modpath('beds') .. '/',
}

--- Functions
-- @section functions

--- Get the look yaw of the player
-- @param pos table
-- @return float
-- @return float
function beds.get_look_yaw(pos)
	local rotation = minetest.get_node(pos).param2
	
	if rotation > 3 then
		rotation = rotation % 4 -- Mask colorfacedir values
	end

	if rotation == 1 then
		return math.pi / 2, rotation
	elseif rotation == 3 then
		return -math.pi / 2, rotation
	elseif rotation == 0 then
		return math.pi, rotation
	else
		return 0, rotation
	end
end

--- Check to see if all players are in bed, and if so, skip to the next day
-- @return boolean
function beds.check_in_beds()
	local num_players = #minetest.get_connected_players()
	return beds.num_sleepers == num_players and num_players > 0
end

--- Get player out of bed
-- @param player  string  player name
-- @param leaving boolean players who have logged out
-- @return void
function beds.get_up(player, leaving)
	local name	  = player:get_player_name()
	local hud_flags = player:hud_get_flags()

	if not player or not name then
		return
	end

	local pos = beds.pos[name] or nil

	if beds.player[name] ~= nil then
		beds.player[name] = nil
		beds.num_sleepers = beds.num_sleepers - 1
	end

	-- prevent sending player specific changes (used for leaving players)
	if leaving then
		return
	end

	if pos then
		player:setpos(pos)
	end

	-- physics, eye_offset, etc
	player:set_eye_offset({x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0})
	player:set_look_horizontal(math.random(1, 180) / 100)
	default.player_attached[name] = false
	player:set_physics_override(1, 1, 1)
	default.player_set_animation(player, 'stand' , 30)
	hud_flags.wielditem = true
	player:hud_set_flags(hud_flags)
end

--- Lay down the player and set their spawn
-- @see beds.get_look_yaw
-- @param player string Player name
-- @param pos table the player's position
-- @param bed_pos table the bed's position
-- @return void
function beds.lay_down(player, pos, bed_pos)
	local name	  = player:get_player_name()
	local hud_flags = player:hud_get_flags()

	if not player or not name then
		return
	end

	-- lay down
	beds.player[name] = 1
	beds.pos[name] = pos
	beds.num_sleepers = beds.num_sleepers + 1

	-- physics, eye_offset, etc
	player:set_eye_offset({x = 0, y = -13, z = 0}, {x = 0, y = 0, z = 0})

	local yaw, param2 = beds.get_look_yaw(bed_pos)

	player:set_look_horizontal(yaw)

	local dir = minetest.facedir_to_dir(param2)
	local p = {x = bed_pos.x + dir.x / 2, y = bed_pos.y, z = bed_pos.z + dir.z / 2}

	player:set_physics_override(0, 0, 0)
	player:setpos(p)

	default.player_attached[name] = true
	default.player_set_animation(player, 'lay' , 0)

	hud_flags.wielditem = false
	player:hud_set_flags(hud_flags)
end

--- Update the formspec to show current status
-- @param finished boolean
-- @return void
function beds.update_formspecs(finished)
	local ges = #minetest.get_connected_players()
	local form_n
	local is_majority = (ges / 2) < beds.num_sleepers

	if finished then
		form_n = beds.formspec .. 'label[2.7,11; Good morning.]'
	else
		form_n = beds.formspec .. 'label[2.2,11;' .. tostring(beds.num_sleepers) ..
			' of ' .. tostring(ges) .. ' players are in bed]'
		if is_majority and beds.night then
			form_n = form_n .. 'button_exit[2,8;4,0.75;force;Force night skip]'
		end
	end

	for name, _ in pairs(beds.player) do
		minetest.show_formspec(name, 'beds_form', form_n)
	end
end

--- Wake up all players and get them out of bed(Coffee?)
-- @see beds.get_up
-- @return void
function beds.kick_players()
	for name, _ in pairs(beds.player) do
		local player = minetest.get_player_by_name(name)
		beds.get_up(player, false)
	end
end

--- Advance to the next day, denying many mobs player flesh
-- @return void
function beds.skip_night()
	minetest.set_timeofday(0.23)
end

--- Right clicking a bed can result a few different outcomes. If it's day,
-- a message informing the player they cannot get sleep, and nothing more. If
-- it's night, the player can get into bed, and if the bed it not protected
-- from them, it will get set as their new spawn location. If they are in the
-- bed, they will get out of it.
--
-- @see beds.get_up
-- @see beds.lay_down
-- @see beds.update_formspecs
-- @see beds.skip_night
-- @see beds.kick_players
-- @see beds.check_in_beds
-- @param pos table position of bed
-- @param player table player
-- @return void
function beds.on_rightclick(pos, player)
	local name = player:get_player_name()
	local ppos = player:getpos()
	local tod  = minetest.get_timeofday()

	if tod > 0.2 and tod < 0.805 then
		if beds.player[name] then
			beds.get_up(player, false)
		end

		minetest.chat_send_player(name, 'You can only sleep at night.')

		return
	end

	-- move to bed
	if not beds.player[name] then
		beds.lay_down(player, ppos, pos)
		-- set spawn position if not protected
		if not minetest.is_protected(player:getpos(), name) then
			local pos = player:getpos()
			beds.spawns[name] = player:getpos()
			minetest.chat_send_player(
				name,
				'Bed spawn point set to ' ..
				string.format('%.1f, %.1f, %.1f', pos.x, pos.y, pos.z)
			)
		end
	else
		beds.get_up(player, false)
	end

	if not beds.single_player then
		beds.update_formspecs(false)
	end

	-- skip the night and let all players stand up
	if beds.check_in_beds() then
		minetest.after(2, function()
			if not beds.single_player then
				beds.update_formspecs(beds.night_skip)
			end
			if beds.night_skip then
				beds.skip_night()
				beds.kick_players()
			end
		end)
	end
end

--- respawn player at bed if enabled and valid position is found(was it all a dream?)
-- @param player table player ObjectRef
-- @return boolean
function beds.respawnplayer(player)
  -- if respawn enabled
	if not beds.respawn then
		return
	end

	local name = player:get_player_name()
	local pos  = beds.spawns[name]

	if pos then
		player:setpos(pos)
		return true
	end
end

--- Player leaves bed
-- @see beds.get_up
-- @see beds.check_in_beds
-- @see beds.update_formspecs
-- @see beds.skip_night
-- @see beds.kick_players
-- @param player table player ObjectRef
-- @return void
function beds.on_leaveplayer(player)
	local name = player:get_player_name()

	beds.get_up(player, true)
	beds.player[name] = nil

	if not beds.check_in_beds() then
		return
	end

	minetest.after(2, function()
		beds.update_formspecs(beds.night_skip)

		if not beds.night_skip then
	  		return
		end

		beds.skip_night()
		beds.kick_players()
	end)
end

--- Process data from beds form
-- @see beds.get_up
-- @see beds.update_formspec
-- @see beds.kick_players
-- @param player table player ObjectRef
-- @param formname string
-- @param fields table
-- @return void
function beds.form_handler(player, formname, fields)
	if formname ~= 'beds_form' then
		return
	end

	if fields.quit or fields.leave then
		beds.get_up(player, false)
		beds.update_formspecs(false)
	end

	if not fields.force then
		return
	end

	beds.update_formspecs(beds.night_skip)

	if beds.night_skip then
		beds.skip_night()
		beds.kick_players()
	end
end

--- Remove player from the list of bed spawns. This might be used by a player,
-- or more likely for removing, expiring, or banning, a player from the system.
-- @within api
-- @param name string
-- @return viod
function beds.remove_player(name)
	beds.spawn[name] = nil
end

-- Import from the minetest_games beds mod
-- @return void
function beds.importDefaultformat()
	local file        = minetest.get_worldpath() .. '/beds_spawns'
	local backup_file = file .. '.import'
	local input, err  = io.open(file, 'r')

	if not input then
		return
	end

	repeat
		local name, pos = {}
		pos.x, pos.y, pos.z, name = input:read('*n', '*n', '*n', '*l')

		if not pos.x or not name then
			break
		end

		beds.spawns[name:sub(2)] = pos
	until input:read(0) == nil

	io.close(input)

	local ret, err os.rename(file, backup_file)
	assert(err == nil, err)
end

-- Export data into the beds spawn file for the minetest_games beds mod
-- @return void
function beds.exportDefaultformat()
	if not beds.respawn then
		return
	end

	if not beds.spawns:len() then
		return
	end

	local data   = {}
	local file   = minetest.get_worldpath() .. '/beds_spawns.export'
	local output = io.open(file, 'w')
	local spawns = beds.spawns:export()

	for name, pos in pairs(spawns) do
		table.insert(
			data,
			string.format('%.1f %.1f %.1f %s\n', pos.x, pos.y, pos.z, name)
		)
	end

	output:write(table.concat(data))
	io.close(output)
end

--- Set the various callbacked required
-- @see beds.respawnplayer
-- @see beds.on_leaveplayer
-- @see beds.form_handler
-- @return void
function beds.set_callbacks()
	minetest.register_on_respawnplayer(beds.respawnplayer)
	minetest.register_on_leaveplayer(beds.on_leaveplayer)
	minetest.register_on_player_receive_fields(beds.form_handler)
end

--- Destroy bed
-- @within api
-- @param pos table
-- @param n int section 1 or 2 of the bed
-- @return void
function destruct_bed(pos, n)
	local node = minetest.get_node(pos)
	local other

	if n == 2 then
		local dir = minetest.facedir_to_dir(node.param2)
		other = vector.subtract(pos, dir)
	elseif n == 1 then
		local dir = minetest.facedir_to_dir(node.param2)
		other = vector.add(pos, dir)
	end

	if beds.reverse then
		beds.reverse = not beds.reverse
		minetest.remove_node(other)
		minetest.check_for_falling(other)
	else
		beds.reverse = not beds.reverse
	end
end

--- Register bed node
-- @within api
-- @param name string
-- @param def table
-- @return void
function beds.register_bed(name, def)
	minetest.register_node(name .. '_bottom', {
		description       = def.description,
		inventory_image   = def.inventory_image,
		wield_image       = def.wield_image,
		drawtype          = 'nodebox',
		tiles             = def.tiles.bottom,
		paramtype         = 'light',
		paramtype2        = 'facedir',
		is_ground_content = false,
		stack_max         = 1,
		groups            = {
			choppy                  = 2,
			oddly_breakable_by_hand = 2,
			flammable               = 3,
			bed                     = 1
		},
		sounds            = def.sounds or default.node_sound_wood_defaults(),
		node_box          = {
			type  = 'fixed',
			fixed = def.nodebox.bottom,
		},
		selection_box     = {
			type  = 'fixed',
			fixed = def.selectionbox,
		},
		on_place          = function(itemstack, placer, pointed_thing)
			local under = pointed_thing.under
			local node  = minetest.get_node(under)
			local udef  = minetest.registered_nodes[node.name]

			if udef and udef.on_rightclick and
					not (placer and placer:is_player() and
					placer:get_player_control().sneak) then
				return udef.on_rightclick(under, node, placer, itemstack,
					pointed_thing) or itemstack
			end

			local pos

			if udef and udef.buildable_to then
				pos = under
			else
				pos = pointed_thing.above
			end

			local player_name = placer and placer:get_player_name() or ''

			if minetest.is_protected(pos, player_name) and
					not minetest.check_player_privs(player_name, 'protection_bypass') then
				minetest.record_protection_violation(pos, player_name)
				return itemstack
			end

			local node_def = minetest.registered_nodes[minetest.get_node(pos).name]

			if not node_def or not node_def.buildable_to then
				return itemstack
			end

			local dir = placer and placer:get_look_dir() and
				minetest.dir_to_facedir(placer:get_look_dir()) or 0
			local botpos = vector.add(pos, minetest.facedir_to_dir(dir))

			if minetest.is_protected(botpos, player_name) and
					not minetest.check_player_privs(player_name, 'protection_bypass') then
				minetest.record_protection_violation(botpos, player_name)
				return itemstack
			end

			local botdef = minetest.registered_nodes[minetest.get_node(botpos).name]

			if not botdef or not botdef.buildable_to then
				return itemstack
			end

			minetest.set_node(pos, {name = name .. '_bottom', param2 = dir})
			minetest.set_node(botpos, {name = name .. '_top', param2 = dir})

			if not (creative and creative.is_enabled_for
					and creative.is_enabled_for(player_name)) then
				itemstack:take_item()
			end
			return itemstack
		end,

		on_destruct = function(pos)
			destruct_bed(pos, 1)
		end,

		on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
			beds.on_rightclick(pos, clicker)
			return itemstack
		end,

		on_rotate = function(pos, node, user, mode, new_param2)
			local dir = minetest.facedir_to_dir(node.param2)
			local p = vector.add(pos, dir)
			local node2 = minetest.get_node_or_nil(p)

			if not node2 or not minetest.get_item_group(node2.name, 'bed') == 2 or
					not node.param2 == node2.param2 then
				return false
			end

			if minetest.is_protected(p, user:get_player_name()) then
				minetest.record_protection_violation(p, user:get_player_name())
				return false
			end

			if mode ~= screwdriver.ROTATE_FACE then
				return false
			end

			local newp = vector.add(pos, minetest.facedir_to_dir(new_param2))
			local node3 = minetest.get_node_or_nil(newp)
			local node_def = node3 and minetest.registered_nodes[node3.name]

			if not node_def or not node_def.buildable_to then
				return false
			end

			if minetest.is_protected(newp, user:get_player_name()) then
				minetest.record_protection_violation(newp, user:get_player_name())
				return false
			end

			node.param2 = new_param2
			-- do not remove_node here - it will trigger destroy_bed()
			minetest.set_node(p, {name = 'air'})
			minetest.set_node(pos, node)
			minetest.set_node(newp, {name = name .. '_top', param2 = new_param2})
			return true
		end,
	})

	minetest.register_node(name .. '_top', {
		drawtype          = 'nodebox',
		tiles             = def.tiles.top,
		paramtype         = 'light',
		paramtype2        = 'facedir',
		is_ground_content = false,
		pointable         = false,
		groups            = {
			choppy                  = 2,
			oddly_breakable_by_hand = 2,
			flammable               = 3,
			bed                     = 2
		},
		sounds            = def.sounds or default.node_sound_wood_defaults(),
		drop              = name .. '_bottom',
		node_box          = {
			type  = 'fixed',
			fixed = def.nodebox.top,
		},
		on_destruct = function(pos)
			destruct_bed(pos, 2)
		end,
	})

	minetest.register_alias(name, name .. '_bottom')

	minetest.register_craft({
		output = name,
		recipe = def.recipe
	})
end

-- Registery the beds that come with this mod.
-- @return void
function beds.beds()
	beds.register_bed('beds:fancy_bed', {
		description = 'Fancy Bed',
		inventory_image = 'beds_bed_fancy.png',
		wield_image = 'beds_bed_fancy.png',
		tiles = {
			bottom = {
				'beds_bed_top1.png',
				'default_wood.png',
				'beds_bed_side1.png',
				'beds_bed_side1.png^[transformFX',
				'default_wood.png',
				'beds_bed_foot.png',
			},
			top = {
				'beds_bed_top2.png',
				'default_wood.png',
				'beds_bed_side2.png',
				'beds_bed_side2.png^[transformFX',
				'beds_bed_head.png',
				'default_wood.png',
			}
		},
		nodebox = {
			bottom = {
				{-0.5, -0.5, -0.5, -0.375, -0.065, -0.4375},
				{0.375, -0.5, -0.5, 0.5, -0.065, -0.4375},
				{-0.5, -0.375, -0.5, 0.5, -0.125, -0.4375},
				{-0.5, -0.375, -0.5, -0.4375, -0.125, 0.5},
				{0.4375, -0.375, -0.5, 0.5, -0.125, 0.5},
				{-0.4375, -0.3125, -0.4375, 0.4375, -0.0625, 0.5},
			},
			top = {
				{-0.5, -0.5, 0.4375, -0.375, 0.1875, 0.5},
				{0.375, -0.5, 0.4375, 0.5, 0.1875, 0.5},
				{-0.5, 0, 0.4375, 0.5, 0.125, 0.5},
				{-0.5, -0.375, 0.4375, 0.5, -0.125, 0.5},
				{-0.5, -0.375, -0.5, -0.4375, -0.125, 0.5},
				{0.4375, -0.375, -0.5, 0.5, -0.125, 0.5},
				{-0.4375, -0.3125, -0.5, 0.4375, -0.0625, 0.4375},
			}
		},
		selectionbox = {-0.5, -0.5, -0.5, 0.5, 0.06, 1.5},
		recipe = {
			{'', '', 'group:stick'},
			{'wool:red', 'wool:red', 'wool:white'},
			{'group:wood', 'group:wood', 'group:wood'},
		},
	})

	-- @within api
	beds.register_bed('beds:bed', {
		description = 'Simple Bed',
		inventory_image = 'beds_bed.png',
		wield_image = 'beds_bed.png',
		tiles = {
			bottom = {
				'beds_bed_top_bottom.png^[transformR90',
				'default_wood.png',
				'beds_bed_side_bottom_r.png',
				'beds_bed_side_bottom_r.png^[transformfx',
				'beds_transparent.png',
				'beds_bed_side_bottom.png'
			},
			top = {
				'beds_bed_top_top.png^[transformR90',
				'default_wood.png',
				'beds_bed_side_top_r.png',
				'beds_bed_side_top_r.png^[transformfx',
				'beds_bed_side_top.png',
				'beds_transparent.png',
			}
		},
		nodebox = {
			bottom = {-0.5, -0.5, -0.5, 0.5, 0.06, 0.5},
			top = {-0.5, -0.5, -0.5, 0.5, 0.06, 0.5},
		},
		selectionbox = {-0.5, -0.5, -0.5, 0.5, 0.06, 1.5},
		recipe = {
			{'wool:red', 'wool:red', 'wool:white'},
			{'group:wood', 'group:wood', 'group:wood'}
		},
	})

	-- Aliases for PilzAdam's beds mod
	minetest.register_alias('beds:bed_bottom_red', 'beds:bed_bottom')
	minetest.register_alias('beds:bed_top_red', 'beds:bed_top')

	-- Fuel
	minetest.register_craft({
		type = 'fuel',
		recipe = 'beds:fancy_bed_bottom',
		burntime = 13,
	})

	minetest.register_craft({
		type = 'fuel',
		recipe = 'beds:bed_bottom',
		burntime = 12,
	})
end

-- Initialize the beds mod.
-- @return void
function beds.init()
	beds.beds()
	beds.set_callbacks()

	local spawn_file = io.open(minetest.get_worldpath() .. '/beds_spawns', 'r')

	if spawn_file ~= nil then
		minetest.log('info', '[beds] beds.importDefaultformat()')
		io.close(spawn_file)
		beds.importDefaultformat()
	end

	minetest.log('[beds] Mode loaded.')
end

beds.init()

-- vim: set autoindent noexpandtab tabstop=4 shiftwidth=4 softtabstop=-1:
